/*
 * Copyright (c) gitee.com/jiandong001.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import HashMap from '@ohos.util.HashMap';
import { ChunkListener } from './MultipartStreamReader';
import { MultipartStreamReader } from './MultipartStreamReader';
import prompt from '@ohos.prompt';
import hilog from '@ohos.hilog';
//import request from '@ohos.request';
import http from '@ohos.net.http'
import fileio from '@ohos.fileio';

import { DevBundleDownloadListener } from './DevBundleDownloadListener';

export class BundleInfo {
  private static readonly TAG = "React BundleInfo"
  private mUrl?: string;
  private mFilesChangedCount: number;

  public static fromJSONString(jsonStr: string): BundleInfo | null {
    if (jsonStr == null) {
      return null;
    }

    let info = new BundleInfo();

    try {
      const obj = JSON.parse(jsonStr);
      info.mUrl = obj.url;
      info.mFilesChangedCount = obj.filesChangedCount;
    } catch (e) {
      hilog.error(0x0, BundleInfo.TAG, "Invalid bundle info: ", (e as Error).message);
      return null;
    }
    return info;
  }

  public toJSONString(): string | null {
    let obj = {
      url: this.mUrl,
      filesChangedCount: this.mFilesChangedCount
    }

    try {
      return JSON.stringify(obj)
    } catch (e) {
      hilog.error(0x0, BundleInfo.TAG, "Can't serialize bundle info: ", (e as Error).message);
      return null;
    }
  }

  public setUrl(url: string): void {
    if (this.mUrl != url) this.mUrl = url;
  }

  public getUrl(): string {
    return this.mUrl != null ? this.mUrl : "unknown";
  }

  public getFilesChangedCount(): number {
    return this.mFilesChangedCount;
  }

  public setFilesChangedCount(count: number): void {
    this.mFilesChangedCount = count;
  }
}

export class BundleDownloader {
  private static readonly TAG = "React BundleDownloader";

  // Should be kept in sync with constants in RCTJavaScriptLoader.h
  private static readonly FILES_CHANGED_COUNT_NOT_BUILT_BY_BUNDLER = -2;
  //  private readonly mClient: OkHttpClient;
  //  private mDownloadBundleFromURLCall?: Call;

  //  private readonly mRequest: http.HttpRequest
  //
  //  public constructor(request: http.HttpRequest) {
  //    this.mRequest = request;
  //  }


  //  public constructor(client: OkHttpClient) {
  //    this.mClient = client;
  //  }

  private static storePlainJSInFile(body: string | ArrayBuffer, headers: Object, outputFile: string): boolean | never //throws IOException
  {

    hilog.info(0x0, BundleDownloader.TAG, 'storePlainJSInFile: ' + " outputFile: " + outputFile);
    if(headers == null || headers == undefined ) return false

    let contentType = headers['content-type'] as string
    if (contentType != undefined) {
      hilog.info(0x0, BundleDownloader.TAG, 'storePlainJSInFile: ' + " contentType: " + contentType.toString());
      if (contentType.includes('application/octet-stream')) {

      } else if (contentType.includes('UTF-8')) { //todo: process other type
        let data = body as string
        // /data/app/el2/100/base/com.example.myapplicationstage/haps/entry/files
        hilog.info(0x0, BundleDownloader.TAG, 'storePlainJSInFile: ' + " outputFile: " + outputFile);
        let writeFd = fileio.openSync(outputFile, 0o2 | 0o100, 0o666)
        let stream = fileio.fdopenStreamSync(writeFd, "w+")
        let writelength = stream.writeSync(data, {
          offset: 0,
          length: data.length,
          position: 0,
          encoding: 'utf-8'
        })
        prompt.showToast({ message: 'write_fileIo success =>' + body, duration: 2000 })
        hilog.info(0x0, BundleDownloader.TAG, 'FileIo WriteLength = ' + writelength)
        stream.close()
        fileio.close(writeFd)
        return true;
      }
    }
    return false;
  }

  private static populateBundleInfo(url: string, headers: Object, bundleInfo: BundleInfo): void {
    bundleInfo.setUrl(url);

    let filesChangedCountStr: string = headers["X-Metro-Files-Changed-Count"];
    if (filesChangedCountStr != undefined && filesChangedCountStr != null) {
      try {
        bundleInfo.setFilesChangedCount(Number(filesChangedCountStr));
      } catch (e) {
        bundleInfo.setFilesChangedCount(BundleDownloader.FILES_CHANGED_COUNT_NOT_BUILT_BY_BUNDLER);
      }
    }
  }

  public downloadBundleFromURL(
    callback: DevBundleDownloadListener,
    //    outputFile: File,
    outputFilePath: string,
    bundleURL: string,
    options: http.HttpRequestOptions,
    bundleInfo: BundleInfo,
    request: http.HttpRequest = http.createHttp()): void {
    //    requestBuilder: Request.Builder = new Request.Builder()): void {

    // http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=com.helloworld&modulesOnly=false&runModule=true

    hilog.info(0x0, BundleDownloader.TAG, 'downloadBundleFromURL: ' + " outputFilePath: " + outputFilePath + " url: " + bundleURL);

    //    request.on('headersReceive', (header) => {
    //      hilog.info(0x0, BundleDownloader.TAG, 'header: ' + JSON.stringify(header));
    //    });

    request.request(bundleURL, options, (err, data) => {
      if (!err) {
        // data.result contains the HTTP response. Parse the response based on service requirements.
        hilog.info(0x0, BundleDownloader.TAG, 'Result:' + data.result);
        hilog.info(0x0, BundleDownloader.TAG, 'code:' + data.responseCode);
        // data.header contains the HTTP response header. Parse the content based on service requirements.
        hilog.info(0x0, BundleDownloader.TAG, 'header:' + JSON.stringify(data.header));
        hilog.info(0x0, BundleDownloader.TAG, 'cookies:' + data.cookies); // 8+

        //todo:should enable parse http response header string to get boundary content
        //        let contentType: string = data.header['content-type']
        //        let patten = new RegExp("multipart/mixed;.*boundary=\"([^\"]+)\"")
        //        let match = patten.exec(contentType);
        //        let matchs = contentType.matchAll(patten)

        let match: string[] = ["3beqjf3apnqeu3h5jqorms4i"]

        if ( false && match.length > 0) {
          hilog.info(0x0, BundleDownloader.TAG, 'boundary:' + match[0]); // 8+ //3beqjf3apnqeu3h5jqorms4i
          this.processMultipartResponse(bundleURL, data, match[0], outputFilePath, bundleInfo, callback)
        } else {
          this.processBundleResult(bundleURL, data, outputFilePath, bundleInfo, callback)
        }

        //                // Make sure the result is a multipart response and parse the boundary.
        //                String contentType = response.header("content-type");
        //                Pattern regex = Pattern.compile("multipart/mixed;.*boundary=\"([^\"]+)\"");
        //                Matcher match = regex.matcher(contentType);
        //                try (Response r = response) {
        //                  if (match.find()) {
        //                    processMultipartResponse(url, r, match.group(1), outputFile, bundleInfo, callback);
        //                  } else {
        //                    // In case the server doesn't support multipart/mixed responses, fallback to normal
        //                    // download.
        //                    processBundleResult(
        //                        url,
        //                        r.code(),
        //                        r.headers(),
        //                        Okio.buffer(r.body().source()),
        //                        outputFile,
        //                        bundleInfo,
        //                        callback);
        //                  }

      } else {
        hilog.info(0x0, BundleDownloader.TAG, 'Could not connect to development server URl:' + bundleURL + 'error:' + JSON.stringify(err));
        callback.onFailure(new Error("Could not connect to development server."));
        request.destroy();
      }
    });
  }

  private processMultipartResponse(
    url: string,
    response: http.HttpResponse,
    boundary: string,
    outputFile: string, //File,
    bundleInfo: BundleInfo,
    callback?: DevBundleDownloadListener): void | never {


    let bodyReader: MultipartStreamReader =
      new MultipartStreamReader(response.result, response.resultType, boundary);

    let completed: boolean =
    bodyReader.readAllParts(
    ((bundleDownloader: BundleDownloader) => {
      return new class temp extends ChunkListener {
        constructor(private bundleDownloader: BundleDownloader) {
          super()
        }
        //@Override
        public onChunkComplete(headers: HashMap<string, string>, body: string | Object | ArrayBuffer, bodyType: http.HttpDataType, isLastChunk: boolean): void | never {
          // This will get executed for every chunk of the multipart response. The last chunk
          // (isLastChunk = true) will be the JS bundle, the other ones will be progress
          // events
          // encoded as JSON.
          if (isLastChunk) {
            // The http status code for each separate chunk is in the X-Http-Status header.
            let status = response.responseCode;
            if (headers.get("X-Http-Status")) {
              status = Number(headers.get("X-Http-Status"));
            }
            bundleDownloader.processBundleResult(url, response, outputFile, bundleInfo, callback); // status, Headers.of(headers), body
          } else {
            if (!headers.hasKey("Content-Type")
            || headers.get("Content-Type") != "application/json") {
              return;
            }

            //                      try {
            //                        JSONObject progress = new JSONObject(body.readUtf8());
            //                        String status =
            //                            progress.has("status") ? progress.getString("status") : "Bundling";
            //                        Integer done = null;
            //                        if (progress.has("done")) {
            //                          done = progress.getInt("done");
            //                        }
            //                        Integer total = null;
            //                        if (progress.has("total")) {
            //                          total = progress.getInt("total");
            //                        }
            //                        callback.onProgress(status, done, total);
            //                      } catch (JSONException e) {
            //                        FLog.e(ReactConstants.TAG, "Error parsing progress JSON. " + e.toString());
            //                      }
          }
        }

        //@Override
        public onChunkProgress(headers: HashMap<string, string>, loaded: number, total: number): void | never {
          if (headers.get("Content-Type").includes('application\/javascript')) {
            //                      callback.onProgress("Downloading", loaded / 1024, total / 1024))
          }
        }
      }(bundleDownloader)
    })(this));


    if (!completed) {
      callback.onFailure(
        new Error(
          "Error while reading multipart response.\n\nResponse code: "
          + response.responseCode
          + "\\n\\n"
          + "URL: "
          + url
          + "\n\n"));
    }
  }

  private processBundleResult(
    url: string,
    response: http.HttpResponse,
    outputFile: string, //File,
    bundleInfo: BundleInfo,
    callback: DevBundleDownloadListener): void | never {
    if (response.responseCode != 200) {
      callback.onFailure(new Error("The development server returned response error code" + response.responseCode))
      return;
    }

    if (bundleInfo != null) {
      BundleDownloader.populateBundleInfo(url, response.header, bundleInfo);
    }

    let temFilePath = outputFile + ".tmp"
    if (BundleDownloader.storePlainJSInFile(response.result as string | ArrayBuffer, response.header, temFilePath)) {
      fileio.renameSync(temFilePath, outputFile)
      callback.onSuccess();
    }
  }
}
